/**
 * 工具集
 * @author 木人心
 * @version 1.2.3
 */
import { matchUnit } from '@/utils/object-util/match-unit'
import { AssignDeepOption, DeepForEachEachCallback, DeepForEachResolveFn } from '@utils/object-util/type'

export class ObjectUtil {
  /**
   * 深层循环，可用于代替一部分递归的使用
   * 常见数据处理：
   * 双数组合并成一个children字段
   * deepForEach([{ groupList: [], deviceList: [] }], (item, resolve, parent) => {
   *   if (parent) parent.children.unshift(item)
   *   item.children = [...item.deviceList]
   *   if (item.groupList.length) resolve(item.groupList.reverse())
   * })
   */
  static deepForEach<T = any, R = T, P = T> (arraySource: T[], callback: DeepForEachEachCallback<T, P>): R[] {
    type CycleType = { value: T, parent: P, level: number }
    const cyclePool: CycleType[] = arraySource.map(item => ({ value: item, parent: null, level: 0 }))
    function createResolveFn (forItem): DeepForEachResolveFn<T, P> {
      return (children, parent?) => {
        cyclePool.push(...children.map(item => ({ value: item, parent: parent || forItem.value, level: forItem.level + 1 })))
      }
    }
    for (let i = 0; i < cyclePool.length; i++) {
      const item = cyclePool[i]
      callback(item.value, createResolveFn(item), item.parent, item.level)
    }
    return arraySource as unknown as R[]
  }

  /**
   * 延迟
   * @param delay
   */
  static sleep (delay: number) {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(undefined)
      }, delay)
    })
  }

  /**
   * 根据地址获取源数据中的值
   * @param source
   * @param address
   * @param char
   */
  static getAttr (source: any, address: string, char = '/'): any {
    const tier = address.split(char)
    try {
      tier.forEach(key => {
        source = source[key]
      })
      return source
    } catch (e) {
      return undefined
    }
  }

  /**
   * 判断参数是否为null|undefined|''
   * @param value
   */
  static isNotEmpty (value: unknown): boolean {
    return value === 0 || !!value
  }

  /**
   * 判断参数是否为null|undefined|''
   * @param value
   */
  static isEmpty (value: unknown): boolean {
    return !this.isNotEmpty(value)
  }

  /**
   * 如果value不为空返回value否则返回arg
   * @param value
   * @param arg
   */
  static isEmptyToVal<T, P> (value: T, arg: P): T | P {
    const result = this.isNotEmpty(value)
    return result ? value : arg
  }

  /**
   * 根据source更新target中的属性值
   * @param target
   * @param source
   * @param template
   */
  static updateVal (target: any, source: any, template?: any): void {
    !template && (template = target)
    for (const key in template) {
      target[key] = source[key]
    }
  }

  /**
   * 深度拷贝 - 不会拷贝函数，不能拷贝自引用的对象，无法正确拷贝非JSON的对象如Date
   * @param arg
   */
  static copy<T> (arg: T): T {
    return JSON.parse(JSON.stringify(arg))
  }

  /**
   * 深度拷贝 - 会保留拷贝函数，不能拷贝自引用的对象，不可以拷贝非JSON的对象如Date
   * @param from
   */
  static deepCopy<T> (from: T): T {
    return this._deepCopy_(from)
  }

  /**
   * deepCopy的递归函数
   * @param from
   * @param to
   * @private
   */
  private static _deepCopy_ (from: any, to?: any): any {
    if (Array.isArray(from)) {
      to = []
      from.forEach((item: unknown, index: number) => {
        to[index] = typeof from[index] === 'object' ? this._deepCopy_(from[index], to[index]) : from[index]
      })
    } else {
      if (!this.isObject(from)) {
        return from
      }
      to = {}
      for (const key in from) {
        to[key] = typeof from[key] === 'object' ? this._deepCopy_(from[key], to[key]) : from[key]
      }
    }
    return to
  }

  /**
   * 深度拷贝 - 不能包含函数，可以拷贝自引用的对象，可以拷贝非JSON的对象如Date
   * @param from
   */
  static broadcastCopy<T> (from: T): Promise<T> {
    return new Promise<T>((resolve, reject) => {
      try {
        const name = 'broadcastCopy' + Date.now() + (Math.random() * 1000).toFixed(2)
        const broadcastChannel = new BroadcastChannel(name)
        broadcastChannel.addEventListener('message', (e) => {
          resolve(e.data)
        })
        new BroadcastChannel(name).postMessage(from)
        broadcastChannel.close()
      } catch (e) {
        reject(e)
      }
    })
  }

  /**
   * 合并两个对象
   * @param target
   * @param source
   * @param option
   */
  static assignDeep<T, U> (target: T, source: U, option?: AssignDeepOption): T & U {
    // deep: 允许添加新的属性, object: 深度合并对象, array: 深度合并数组
    const _option_ = Object.assign({
      deep: true,
      object: true,
      array: true
    }, option || {})
    this._assignDeep_(target, source, _option_)
    return <T & U>target
  }

  /**
   * assignDeep 的递归函数
   * @param target
   * @param source
   * @param option
   * @private
   */
  private static _assignDeep_ (target: any, source: any, option: AssignDeepOption): void {
    for (const key in source) {
      const val = source[key]
      if ((this.isObject(val) && this.isObject(target[key]) && option.object) || (Array.isArray(val) && Array.isArray(target[key]) && option.array)) {
        this._assignDeep_(target[key], val, option)
      } else {
        // eslint-disable-next-line no-prototype-builtins
        if (option.deep || target.hasOwnProperty(key)) {
          target[key] = val
        }
      }
    }
  }

  static matchUnit = matchUnit

  static isBoolean (arg: unknown): arg is boolean {
    return Object.prototype.toString.call(arg) === '[object Boolean]'
  }

  static isString (arg: unknown): arg is string {
    return Object.prototype.toString.call(arg) === '[object String]'
  }

  static isNumber (arg: unknown): arg is number {
    return Object.prototype.toString.call(arg) === '[object Number]'
  }

  static isObject (arg: unknown): arg is Global.Object {
    return Object.prototype.toString.call(arg) === '[object Object]'
  }

  static isDate (arg: unknown): arg is Date {
    return Object.prototype.toString.call(arg) === '[object Date]'
  }

  static isUndefined (arg: unknown): arg is undefined {
    return typeof arg === 'undefined'
  }
}

export class NumberUtil {
  /**
   * 保留最大小数位数
   * @param value
   * @param fractionDigits
   */
  static toFixed (value: string | number, fractionDigits: number): number {
    return Number(Number(value).toFixed(fractionDigits))
  }
}

export class FileUtil {
  static compressImage (option: { data: typeof Image | File | Blob, width?: number, height?: number, scale?: number, fileName?: string }) {
    // eslint-disable-next-line no-async-promise-executor
    return new Promise<File>(async (resolve) => {
      let image: HTMLImageElement = null
      let fileName: string = null
      if (option.data instanceof HTMLImageElement) {
        image = option.data
      } else {
        if (option.data instanceof File) {
          fileName = option.data.name
        }
        image = await FileUtil.FileToImage(option.data as File | Blob)
      }
      let width = 0
      let height = 0
      if (option.width) {
        width = option.width
        if (option.height) {
          height = option.height
        } else {
          height = option.width / image.width * image.height
        }
      } else if (option.scale) {
        width = image.width * option.scale
        height = image.height * option.scale
      }
      const canvas = document.createElement('canvas')
      canvas.width = width
      canvas.height = height
      const context = canvas.getContext('2d')
      context.drawImage(image, 0, 0, width, height)
      canvas.toBlob((e) => {
        resolve(new File([e], option.fileName || fileName || Date.now().toString()))
      })
    })
  }

  static FileToImage (file: File | Blob) {
    return new Promise<HTMLImageElement>((resolve) => {
      const image = new Image()
      image.src = URL.createObjectURL(file)
      image.onload = () => {
        resolve(image)
      }
    })
  }
}
